<?php

function date_ts($date, $format) {
	$masks = array( 
	  'd' => '(?P<d>[0-9]{2})', 
	  'm' => '(?P<m>[0-9]{2})', 
	  'Y' => '(?P<Y>[0-9]{4})', 
	  'H' => '(?P<H>[0-9]{2})', 
	  'i' => '(?P<i>[0-9]{2})', 
	  's' => '(?P<s>[0-9]{2})', 
	 // usw.. 
	); 
	
	$datedef = array(
		'd' => 1,
		'm' => 1,
		'Y' => 0,
		'H' => 0,
		'i' => 0,
		's' => 0
	);
	
	$rexep = "#".strtr(preg_quote($format), $masks)."#"; 
	if(!preg_match($rexep, $date, $out)) {
		return false; 
	}
	$date = $out+$datedef;
	$ts = mktime($date['H'],$date['i'],$date['s'],intval($date['m']),intval($date['d']),intval($date['Y']));
	return $ts; 
}

abstract class Model {
	

	/**
	 * ----------------------------------
	 * METHODES A DEFINIR
	 * ----------------------------------
	 */
	
	/**
	 * Sauvegarde de l'objet courant
	 */
	public abstract function save();
	
	/**
	 * Chargement d'un objet d'id précis (au moins 1 attribut id, plusieurs supportés)
	 */
	public abstract function load($id);

	/**
	 * Supprime un élément d'id précis (au moins 1 attribut id, plusieurs supportés)
	 */
	public abstract function delete();
	
	/**
	 * Renvoie le schéma de la classe (tableau associatif 'attribut' => 'type')
	 * Les types possibles sont:
	 * - boolean
	 * - bool
	 * - int
	 * - integer
	 * - double
	 * - float
	 * - string
	 * 
	 * Le format peut être le suivant:
	   $schema = array(
		'attr1' => 'string(24)',
		'attr2' => 'int'
	   );
	 * OU
	   $schema = array(
		'attr1' => array('type' => 'string(24)'),
		'attr2' => array('type' => 'int')
	   );
	 *
	 * Le deuxième cas peut inclure les propriétés suivantes:
	 * - primarykey: true si c'est une clé primaire
	 * - type: le type (voir plus haut) Possibilité d'ajouter entre parenthèse la longueur (pour string)
	 * - sqlcolumn: le nom de la colonne SQL correspondante (si non identique à l'attribut)
	 */
	public abstract function schema();
	
	/**
	 * ----------------------------------
	 * FIN METHODES A DEFINIR
	 * ----------------------------------
	 */	

	/**
	 * Le schéma parsé et récupérable correctement
	 */
	protected $schema = array();
	
	protected static $db = null;
	
	/**
	 * Les options par défaut utilisé par la classe
	 * Contient les formats de date et datetime utilisé par défaut
	 * Le format utilisé est celui de date. Seul ces paramètres sont supportés
	 * - d, m, Y, H, i, s
	 *
	 * @link http://www.php.net/manual/fr/function.date.php
	 */
	public static $DEFAULT_OPTIONS = array(
		'date_format_db' => 'Y-m-d',
		'date_format_view' => 'd/m/Y',
		'datetime_format_db' => 'Y-m-d H:i:s',
		'datetime_format_view' => 'd/m/Y H:i:s'
	);
	
	/**
	 * Les noms des constantes correspondants aux valeurs par défaut des options
	 */
	private static $CONSTS_OPTIONS = array(
		'SIMPLEMODEL_DATE_FORMAT_DB' => 'date_format_db',
		'SIMPLEMODEL_DATE_FORMAT_VIEW' => 'date_format_view',
		'SIMPLEMODEL_DATETIME_FORMAT_DB' => 'datetime_format_db',
		'SIMPLEMODEL_DATETIME_FORMAT_VIEW' => 'datetime_format_view'
		
	);
	
	public function options() {
		return array();
	}
	
	
	/**
	 * Chargement d'un objet depuis un Statement.
	 */
	public function fromStatement($rowFetched) {
		$this->init($rowFetched, array('fromDB' => true));
	}
	
	/**
	 * Récupère le schéma déjà parsé, ou le parse si forcé ou si non parsé déjà
	 */
	public function getSchema($forcedParse = false, $className = "") {
		$this->parseSchema($forcedParse, $className = "");
		return $this->schema;
	}
	/**
	 * Parse le schéma de la classe
	 */
	public function parseSchema($forced = false, $className = "") {

		if (!empty($this->schema) && !$forced) {
			return;
		}
		
		$user_schema = $this->schema();
		
		$schema = array();
		foreach ($user_schema as $attrname => $data) {
			$type = $data;
			$primary = false;
			$sqlcolumn = $attrname;
			$length = 0;
			if (is_array($data)) {
				$primary = !empty($data['primarykey']);
				$type = $data['type'];
				if (!empty($data['sqlcolumn'])) {
					$sqlcolumn = $data['sqlcolumn'];
				}
			}
			if (strpos($type, '(') !== false) {
				$pattern = "/([a-zA-Z0-9_-]+)\([0-9+]\)/";
				$matches = array();
				if (preg_match($pattern, $type, $matches)) {
					$type = $matches[1];
					$length = intval($matches[2]);
				}
			}
			$schema[$attrname] = array(
				'primary' => $primary,
				'attribute_name' => $attrname,
				'sqlcolumn_name' => $sqlcolumn,
				'length' => $length,
				'type' => $type
			);
		}
		$this->schema = $schema;
	}
	
	/**
	 * Renvoie true si le tableau en paramètre est un tableau associatif, false sinon
	 */
	protected static function isAssocArray($a) {
		return (bool)count(array_filter(array_keys($a), 'is_string'));
	}
	
	/**
	 * A deporter ou a mieux faire
	 */
	public static function array_str_repeat($str, $times) {
		$vars = array();
		for ($i = 0; $i < $times; $i++) {
			$vars[] = $str;
		}
		return $vars;
	}
	
	/**
	 * Change les valezurs par défaut
	 * Si $overwrite vaut true, seules les options renseignés seront changés (sinon, toutes)
	 */
	public static function setDefaultOptions($options = array(), $overwrite = true) {
		if (!$overwrite) {
			self::$DEFAULT_OPTIONS = $options;
		} else {
			foreach ($options as $option_name => $value) {
				self::$DEFAULT_OPTIONS[$option_name] = $value;
			}
		}
	}
	
	/**
	 * Obtient les options par défaut, en prenant en compte les constantes (en 2ème priorité)
	 */
	public function getOptions($options = array()) {
		$opts = array();
		
		foreach (self::$CONSTS_OPTIONS as $constname => $option_name) {
			if (defined($constname)) {
				$opts[$option_name] = constant($constname);
			}
		}
		foreach (self::$DEFAULT_OPTIONS as $option_name => $value) {
			$opts[$option_name] = $value;
		}
		
		foreach ($this->options() as $option_name => $value) {
			$opts[$option_name] = $value;	
		}
		
		foreach ($options as $option_name => $value) {
			$opts[$option_name] = $value;
		}
		return $opts;
	}
	
	/**
	 * Récupère les options en statique
	 */
	public static function getStaticOptions($options = array()) {
		
		foreach (self::$CONSTS_OPTIONS as $constname => $option_name) {
			if (defined($constname)) {
				$opts[$option_name] = constant($constname);
			}
		}
		foreach (self::$DEFAULT_OPTIONS as $option_name => $value) {
			$opts[$option_name] = $value;
		}
		
		
		foreach ($options as $option_name => $value) {
			$opts[$option_name] = $value;
		}
		return $opts;
	}

	
	
	/**
	 * Fonction à utiliser dans le constructeur
	 * Permet d'initialiser un objet selon le type du paramètre:
	 * - array associative: initFromAssoc
	 * - PDOStatement: fromStatement (à définir)
	 * - EnhancedPDOStatement: fromStatement, récupération du lastFetch automatique
	 * - Autre !empty: load (à définir)
	 *
	 */
	public function init($param, $options = array()) {
		if (is_array($param) && self::isAssocArray($param)) {
			$this->initFromAssoc($param, $options);
		}
		else if ($param instanceof EnhancedPDOStatement || $param instanceof PDOStatement) {
			$rowFetched = $options;
			if ($param instanceof EnhancedPDOStatement && empty($rowFetched)) {
				$rowFetched = $param->lastFetch;
			}
			$this->fromStatement($rowFetched, $param);
		}
		else if (!empty($param)) {
			// On prends tous les paramètres (load avec multiple ID)
			// Attention: les tableaux indexés non associatifs sont pris en compte dans le load
			$params = func_get_args();
			call_user_func_array(array($this, 'load'), $params);
		}
	}
	
	public function getType($attr, $value) {
		$schema = $this->getSchema();

		$type = 'auto';
		if (isset($schema[$attr])) {
			$type = $schema[$attr]['type'];
		}
		
		if ($type == 'auto') {
			$type = self::getDetectedType($value);
		}
		return $type;
	}
	
	/**
	 * Initialise l'objet courant depuis un tableau associatif
	 * Utilise le schéma pour forcer le type, sinon utilisation de getDetectedType (voir plus bas)
	 */
	public function initFromAssoc($a, $options = array()) {
		
		$schema = $this->getSchema();
		$optionsModel = $this->getOptions();
		
		foreach ($a as $name => $value) {
			if (!is_string($name)) {
				continue;
			}
			
			$type = $this->getType($name, $value);

			if (!empty($options['fromDB'])) {
				$value = self::getValueFromDB($type, $value, $optionsModel);
			} else {
				$optionsModel['convertDate'] = false;
				$value = self::getValueFromType($type, $value, $optionsModel);
			}
			
			$this->$name = $value;
		}
	}
	
	/**
	 * Transforme la valeur dans le type voulu
	 * Cas spéciaux:
	 * - boolean / bool pour une string 'true' /'false', transformera en true / false
	 * - boolean / bool pour une chaîne 'on': true
	 *
	 * - double / float avec des ',': transformera la ',' en '.'
	 *
	 * - string: ajout de la chaîne '' au type
	 * - string pour true / false: transformera en 'true' / 'false'
	 * 
	 * - date pour string: convertit la date au format 'user' au format 'db'
	 * - date pour int: convertit le timestamp au format 'db'
	 *
	 * - datetime pour string: convertit la date au format 'user' au format 'db'
	 * - datetime pour int: convertit le timestamp au format 'db'
	 *
	 * Pour int: utilisation de intval
	 * Pour double / float: utilisation de floatval
	 * Pour boolean: utilisation de !empty (si en dehors des cas spéciaux)
	 */
	protected static function getValueFromType($type, $value, $options = array()) {
		$v = $value;
		
		switch($type) {
			case 'double':
			case 'float':
				if (is_string($value) && strpos($value, ',') !== false) {
					$value = str_replace(',','.', $value);
				}
				$v = floatval($value);
				break;
				
			case 'string':
				if ($value === true) {
					$v = 'true';
				} else if ($value === false) {
					$v = 'false';
				} else if (!is_null($value)) {
					$v = $value.'';
				}
				break;
			
			case 'bool':
			case 'boolean':
				if (is_string($value)) {
					switch($value) {
						case 'true':
						case 'on':
							$v = true;
							break;
						case 'false':
							$v = false;
							break;
							
						default:
							$v = !empty($value);
							break;
					}
				} else {
					$v = !empty($value);
				}
				break;
				
			case 'integer':
			case 'int':
				$v = intval($value);
				break;
				
			case 'date':
				if (!isset($options['convertDate']) || $options['convertDate']) {
					$timestamp = $value;
					if (is_string($value)) {
						$timestamp = date_ts($value, $options['date_format_view']);
					}
					$v = date($options['date_format_db'], $timestamp);
				}
				break;
				
			case 'datetime':
				if (!isset($options['convertDate']) || $options['convertDate']) {
					$timestamp = $value;
					if (is_string($value)) {
						$timestamp = date_ts($value, $options['datetime_format_view']);
					}
					$v = date($options['datetime_format_db'], $timestamp);
				}
				break;
		}
		return $v;
	}
	
	/**
	 * Renvoie la valeur obtenue de la DB, avec le bon type (forcé du schema ou non)
	 */
	public function getValueFromDB($type, $value, $options = array()) {
		$v = $value;
		
		switch($type) {
			case 'double':
			case 'float':
				$v = floatval($value);
				break;
				
			case 'string':
				if (!is_null($value)) {
					$v = $value.'';
				}
				break;
			
			case 'bool':
			case 'boolean':
				$v = !empty($value);
				break;
				
			case 'integer':
			case 'int':
				$v = intval($value);
				break;
				
			case 'date':
				$timestamp = $value;
				if (is_string($value)) {
					$timestamp = date_ts($value, $options['date_format_db']);
				}
				$v = date($options['date_format_view'], $timestamp);
				break;
				
			case 'datetime':
				$timestamp = $value;
				if (is_string($value)) {
					$timestamp = date_ts($value, $options['datetime_format_db']);
				}
				$v = date($options['datetime_format_view'], $timestamp);
				break;
		}
		return $v;
	}
	
	
	/** 
	 * Renvoie le type détectée automatiquement
	 * Cas spéciaux:
	 * - numerique avec un .: double
	 * - numerique (autre): integer
	 * - string 'true', 'false': booléen
	 */
	protected static function getDetectedType($value) {
		$type = 'string';
		if (is_numeric($value) && strpos($value, '.') !== false) {
			$type = 'double';
		}
		else if (is_numeric($value)) {
			$type = 'integer';
		}
		else if (is_string($value) && ($value == 'true' || $value == 'false')) {
			$type = 'boolean';
		}
		else {
			$type = gettype($value);
		}

		return $type;
	}

	/**
	 * Renvoie un nouvel objet Database
	 * Met l'attribut pdoDefaultCursor à la valeur PDO::CURSOR_SCROLL (nécessaire pour revenir en arrière)
	 */
	public static function getDB($options = array()) {
		$db = null;
		if (!empty($options['new'])) {
			$db = self::$db;
		}
		if (class_exists('Database') && is_null($db)) {
			$db = new Database($options);		
		}
		return $db;
	}

}